#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

#region Imports

using System.Configuration;
using File = System.IO.File;
using StreamWriter = System.IO.StreamWriter;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Data.SqlClient;
using Prawks.SqlMethods.CodeGeneration;

using Debug = System.Diagnostics.Debug;
using Trace = System.Diagnostics.Trace;
using CodeDomProvider = System.CodeDom.Compiler.CodeDomProvider;
using SysAssembly = System.Reflection.Assembly;
using FileVersionInfo = System.Diagnostics.FileVersionInfo;
using Path = System.IO.Path;
using TextWriter = System.IO.TextWriter;
using StringWriter = System.IO.StringWriter;

#endregion

internal sealed class Application
{
    static void Main(string[] args)
    {
        try
        {
            WriteLogo();
            Console.WriteLine();

            //
            // If no arguments were specified then display usage.
            //

            if (args.Length == 0)
            {
                ProgramOptions.ShowCommandLineUsage();
                return;
            }

            //
            // Parse the command-line arguments.
            //

            ProgramOptions options = ProgramOptions.ParseFromArgs(args);

            //
            // Setup default values for options whose corresponding arguments 
            // were not specified on the command-line.
            //

            if (options.Namespace.Length == 0)
                options.Namespace = options.DatabaseTitle + "Db";

            if (options.TypeName.Length == 0)
                options.TypeName = options.DatabaseTitle + "Commands";

            if (options.OutputFileName.Length == 0)
                options.OutputFileName = options.DatabaseTitle + "Commands";

            if (options.OutputAssemblyName.Length == 0)
                options.OutputAssemblyName = Path.GetFileNameWithoutExtension(options.OutputFileName) + ".dll";

            //
            // Get the code generator and compiler provider based on
            // the target language of the code and generate code.
            //

            CodeDomProvider codeDomProvider;
 
            try
            {
                codeDomProvider = CodeDomProvider.CreateProvider(options.Language);
            }
            catch (ConfigurationException e)
            {
                throw new ProgramException("Unknown target language: " + options.Language, e);
            }

            using (codeDomProvider)
            {
                //
                // If the output file name is missing an extension, then supply
                // the default one by asking the code DOM provider.
                //

                if (Path.HasExtension(options.OutputFileName) == false)
                    options.OutputFileName = Path.ChangeExtension(options.OutputFileName, codeDomProvider.FileExtension);

                options.OutputFileName = Path.GetFullPath(options.OutputFileName);

                //
                // Check that all names are valid in the target language.
                // The namespace identifier cannot be safely validated. For example,
                // the C# code generator thinks that "System.Data" is invalid
                // because the period (.) is not allowed in a C# identifier.
                //

                if (!codeDomProvider.IsValidIdentifier(options.TypeName))
                {
                    throw new ProgramException("The type name '" + 
                        options.TypeName + "' is not a valid identifier in the target language.");
                }

                if (options.ShouldGenerateFactory &&
                    !codeDomProvider.IsValidIdentifier(options.Factory))
                {
                    throw new ProgramException("The factory method name '" + 
                        options.Factory + "' is not a valid identifier in the target language.");
                }

                //
                // Options are done, initialize the application with them.
                //

                Application app = new Application(options);

                //
                // Generate the code DOM.
                //

                CodeCompileUnit codeUnit = app.GenerateCodeUnit();

                //
                // Persist the source code if requested.
                //
                
                if (options.GenerateCode)
                {
                    using (StreamWriter writer = File.CreateText(options.OutputFileName))
                        codeDomProvider.GenerateCodeFromCompileUnit(codeUnit, writer, null);

                    Console.WriteLine();
                    Console.WriteLine("Successfully generated SQL data access method(s) in:");
                    Console.WriteLine(options.OutputFileName);
                }
    
                //
                // Compile the code to an assembly if requested.
                //

                if (options.CompileCode)
                {
                    Console.WriteLine();
                    Console.WriteLine("Compiling...");

                    CompilerResults compilerResults = app.CompileToAssembly(codeDomProvider, codeUnit);

                    Console.WriteLine();
                    Console.WriteLine("Successfully compiled assembly:");
                    Console.WriteLine(compilerResults.PathToAssembly);
                }
            }
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e.GetBaseException().Message);
            Trace.WriteLine(e.ToString());
            Environment.ExitCode = -1;
        }
    }

    private Application(ProgramOptions options)
    {
        Debug.Assert(options != null);

        _options = options;
    }

    public ProgramOptions Options
    {
        get { return _options; }
    }

    private CodeCompileUnit GenerateCodeUnit()
    {
        //
        // Set-up the connection string and open a connection to the database.
        //

        SqlConnectionStringBuilder connectionString = new SqlConnectionStringBuilder();

        connectionString.ApplicationName = Assembly.GetName().Name;
        connectionString.DataSource = _options.Server;
        connectionString.InitialCatalog = _options.Database;
    
        if (_options.UseTrustedConnection &&
            _options.UserID.Length == 0)
        {
            connectionString.IntegratedSecurity = true;
        }
        else
        {
            connectionString.IntegratedSecurity = false;
            connectionString.UserID = _options.UserID;
            connectionString.Password = _options.Password;
        }

        //
        // Create namespace and import other namespaces into it.
        // If no namespace was specified, then assume the global one.
        //

        CodeNamespace codeNamespace = new CodeNamespace(_options.Namespace);

        CodeCommentStatementCollection comments = codeNamespace.Comments;
        comments.Add(new CodeCommentStatement(string.Empty));
        comments.Add(new CodeCommentStatement("This source code was auto-generated by:"));
        comments.Add(new CodeCommentStatement(Assembly.FullName));
        comments.Add(new CodeCommentStatement(string.Empty));

        codeNamespace.Imports.Add(new CodeNamespaceImport("Prawks.SqlMethods"));

        //
        // Define a new abstract class and it to the namespace.
        //

        CodeTypeDeclaration codeType = new CodeTypeDeclaration(_options.TypeName);
        codeType.IsClass = true;
        codeType.TypeAttributes |= System.Reflection.TypeAttributes.Abstract;
        codeType.IsPartial = _options.MarkClassPartial;

        if (_options.BaseTypeName.Length != 0)
            codeType.BaseTypes.Add(_options.BaseTypeName);

        codeNamespace.Types.Add(codeType);

        //
        // Add a default public constructor because the default one
        // generated by the compiler may be marked as protected 
        // (family member access) since the containing class is abstract.
        // The C# compiler, for example, does this.
        //

        CodeConstructor codeConstructor = new CodeConstructor();
        codeConstructor.Attributes = MemberAttributes.Public;
        codeType.Members.Add(codeConstructor);

        //
        // Create the methods of the type from SQL stored procedures.
        //

        SqlMethodsGenerator sqlMethodsGenerator = new SqlMethodsGenerator(codeType);

        sqlMethodsGenerator.UseNullableTypes = _options.UseNullableTypes;
        
        GeneratorFeedback feedback = new GeneratorFeedback();
        sqlMethodsGenerator.GeneratedProcedure += new FeedbackHandler(feedback.OnGeneratedProcedure);

        Console.WriteLine("Generating...");
        Console.WriteLine();

        using (SqlConnection connection = new SqlConnection(connectionString.ToString()))
        {
            connection.Open();

            if (_options.Procedures.Length == 0)
                sqlMethodsGenerator.AddUserProcedures(connection);
            else
                sqlMethodsGenerator.AddProcedures(connection, _options.Procedures);
        }

        //
        // Create a factory method if specified.
        //

        if (_options.ShouldGenerateFactory)
        {
            codeNamespace.Imports.Add(new CodeNamespaceImport("Prawks.CodeProvisioning"));

            CodeMemberMethod codeFactoryMethod = new CodeMemberMethod();

            codeFactoryMethod.Name = _options.Factory;
            codeFactoryMethod.Attributes = MemberAttributes.Public | MemberAttributes.Static;
            codeFactoryMethod.ReturnType = new CodeTypeReference(codeType.Name);
            codeFactoryMethod.Statements.Add(new CodeMethodReturnStatement(new CodeCastExpression(codeType.Name, new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("DynamicTypeImpl"), "CreateInstance", new CodeExpression[] { new CodeTypeOfExpression(codeType.Name) }))));

            codeType.Members.Add(codeFactoryMethod);
        }

        //
        // Put the namespace into a compile unit and generate code from it.
        //

        CodeCompileUnit codeUnit = new CodeCompileUnit();
        codeUnit.Namespaces.Add(codeNamespace);

        return codeUnit;
    }

    private CompilerResults CompileToAssembly(CodeDomProvider codeDomProvider, CodeCompileUnit codeUnit)
    {
        return CompileToAssembly(codeDomProvider, codeUnit, false);
    }

    private CompilerResults CompileToAssembly(CodeDomProvider codeDomProvider, CodeCompileUnit codeUnit, bool dontThrowOnErrors)
    {
        Debug.Assert(codeDomProvider != null);
        Debug.Assert(codeUnit != null);

        CompilerParameters parameters = new CompilerParameters();
                
        foreach (string reference in Configuration.Sections.CompilationAssemblyRefs)
            parameters.ReferencedAssemblies.Add(reference);

        foreach (string reference in _options.AssemblyReferences)
            parameters.ReferencedAssemblies.Add(reference);

        parameters.OutputAssembly = _options.OutputAssemblyName;

        //
        // Invoke the compiler and display results. In case of errors,
        // send the entire compiler output in the message of an exception.
        //

        CompilerResults results = codeDomProvider.CompileAssemblyFromDom(parameters, codeUnit);

        if (!dontThrowOnErrors && results.Errors.HasErrors)
        {
            StringWriter messageWriter = new StringWriter();

            messageWriter.WriteLine("Failed to compile code. The detailed output from the compiler was:");
            messageWriter.WriteLine();

            foreach (string line in results.Output)
                messageWriter.WriteLine(line);

            throw new ProgramException(messageWriter.GetStringBuilder().ToString());
        }

        return results;
    }

    internal static SysAssembly Assembly
    {
        get { return SysAssembly.GetCallingAssembly(); }
    }

    internal static string Title
    {
        get { return Assembly.GetName().Name; }
    }

    internal static Version Version
    {
        get { return Assembly.GetName().Version; }
    }

    internal static FileVersionInfo FileVersionInfo
    {
        get { return FileVersionInfo.GetVersionInfo(Assembly.Location); }
    }
    
    private static void WriteLogo()
    {
        WriteLogo(Console.Out);
    }

    private static void WriteLogo(TextWriter writer)
    {
        writer.WriteLine("{0} V{1}", Title, Version);
        writer.WriteLine(FileVersionInfo.LegalCopyright);
    }

    private sealed class GeneratorFeedback
    {
        private int _count = 0;

        public void OnGeneratedProcedure(object sender, FeedbackEventArgs e)
        {
            Console.WriteLine("{0,5} > {1}", (++_count).ToString(), e.Text);
        }
    }

    private readonly ProgramOptions _options;
}
